﻿using System;
using System.Linq;
using System.Linq.Expressions;
using System.Data.Linq;
using System.Linq.Dynamic;
using System.Globalization;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;

using Fuse8.DomainFramework.Domain;
using Fuse8.DomainFramework.Common;
using Fuse8.DomainFramework.Common.Locator;
using Fuse8.DomainFramework.Common.Exceptions;

namespace Fuse8.DomainFramework.DataAccess.LinqDatabase
{
    /// <summary>
    /// 
    /// </summary>
    /// <typeparam name="TEntity"></typeparam>
    /// <typeparam name="TKey"></typeparam>
    /// <typeparam name="TRepository"></typeparam>
    public class LinqEntityDataRepository<TEntity, TKey, TRepository> : BaseEntityDataRepository<TEntity, TKey, TRepository>, IEntityDataRepository<TEntity, TKey>
        where TEntity : class, IEntityObject<TKey>, new()
        where TKey : IComparable<TKey>, IEquatable<TKey> 
        where TRepository : class
    {
        #region Properties

        /// <summary>
        /// 
        /// </summary>
        protected virtual bool CacheQuery { get; set; }

        /// <summary>
        /// 
        /// </summary>
        protected virtual DataContext CurrentContext
        {
            get
            {
                var unitOfWork = UnitOfWorkController<TEntity>.GetCurrentUnitOfWork() as LinqUnitOfWork<TEntity>;

                if (unitOfWork == null)
                {
                    throw new UnitOfWorkNotDefinedException();
                }

                return unitOfWork.CurrentContext;
            }
        }

        #endregion

        #region Constuctors

        /// <summary>
        /// Use Compiled linq query by default
        /// </summary>
        public LinqEntityDataRepository()
            : this(true)
        { }
        
        /// <summary>
        /// 
        /// </summary>
        /// <param name="cacheQuery"></param>
        public LinqEntityDataRepository(bool cacheQuery)
        {
            CacheQuery = cacheQuery;
        }

        #endregion

        #region BaseEntityDataRepository override

        /// <summary>
        /// 
        /// </summary>
        protected ILinqEntityDataRepositoryMapper<TEntity, TRepository> LinqMapper
        {
            get
            {
                var result = base.Mapper as ILinqEntityDataRepositoryMapper<TEntity, TRepository>;

                if (result == null)
                {
                    throw new EntityDataRepositoryMapperNotDefinedException();
                }

                return result;
            }
        }

        /// <summary>
        /// 
        /// </summary>
        /// <param name="repositoryEntity"></param>
        protected override void AddToRepository(TRepository repositoryEntity)
        {
            Guard.ArgumentNotNull(repositoryEntity, "repositoryEntity");
            
            var currentContext = CurrentContext;
            
            try
            {
                currentContext.GetTable<TRepository>().InsertOnSubmit(repositoryEntity);
                currentContext.SubmitChanges();
            }
            catch(ChangeConflictException)
            {
                ResolveConflicts(currentContext);
            }
        }

        /// <summary>
        /// 
        /// </summary>
        /// <param name="repositoryEntity"></param>
        protected override void SaveToRepository(TRepository repositoryEntity)
        {
            Guard.ArgumentNotNull(repositoryEntity, "repositoryEntity");

            var currentContext = CurrentContext;
            
            try
            {
                currentContext.SubmitChanges();
            }
            catch (ChangeConflictException)
            {
                ResolveConflicts(currentContext);
            }
        }

        /// <summary>
        /// 
        /// </summary>
        /// <param name="repositoryEntity"></param>
        protected override void DeleteFromRepository(TRepository repositoryEntity)
        {
            Guard.ArgumentNotNull(repositoryEntity, "repositoryEntity");

            var currentContext = CurrentContext;

            try
            {
                currentContext.GetTable<TRepository>().DeleteOnSubmit(repositoryEntity);
                currentContext.SubmitChanges();
            }
            catch (ChangeConflictException)
            {
                ResolveConflicts(currentContext);
            }
        }

        /// <summary>
        /// 
        /// </summary>
        /// <param name="id"></param>
        protected override void DeleteFromRepository(TKey id)
        {
            throw new NotImplementedException();
        }

        /// <summary>
        /// 
        /// </summary>
        /// <param name="entityObject"></param>
        public override void Save(TEntity entityObject)
        {
            Guard.ArgumentNotNull(entityObject, "entityObject");

            TRepository repositoryEntity = ConvertAndAttach(entityObject);

            SaveToRepository(repositoryEntity);
        }

        /// <summary>
        /// 
        /// </summary>
        /// <param name="entityObject"></param>
        public override void Remove(TEntity entityObject)
        {
            Guard.ArgumentNotNull(entityObject, "entityObject");

            TRepository repositoryEntity = ConvertAndAttach(entityObject);

            if (repositoryEntity != null)
            {
                DeleteFromRepository(repositoryEntity);
            }
        }

        /// <summary>
        /// 
        /// </summary>
        /// <param name="id"></param>
        public override void Remove(TKey id)
        {
            TRepository repositoryEntity = ConvertAndAttach(id);

            if (repositoryEntity != null)
            {
                DeleteFromRepository(repositoryEntity);
            }
        }

        /// <summary>
        /// 
        /// </summary>
        /// <param name="expression"></param>
        /// <returns></returns>
        public override TEntity FindOne(Expression<Func<TEntity, bool>> expression)
        {
            Guard.ArgumentNotNull(expression, "expression");

            return ConstructLinqQuery(expression).FirstOrDefault();
        }

        /// <summary>
        /// 
        /// </summary>
        /// <param name="expression"></param>
        /// <returns></returns>
        public override IQueryable<TEntity> Find(Expression<Func<TEntity, bool>> expression)
        {
            Guard.ArgumentNotNull(expression, "expression");

            return ConstructLinqQuery(expression);
        }

        /// <summary>
        /// 
        /// </summary>
        /// <returns></returns>
        public override IQueryable<TEntity> FindAll()
        {
            return ConstructLinqQuery(null);
        }

        /// <summary>
        /// 
        /// </summary>
        /// <returns></returns>
        protected override IQueryable<TEntity> AsQueryable()
        {
            return CreateCommonQuery(CurrentContext, null);
        }

        #endregion

        #region Methods

        /// <summary>
        /// 
        /// </summary>
        /// <param name="id"></param>
        /// <returns></returns>
        protected virtual TRepository ConvertAndAttach(TKey id)
        {
            TRepository repositoryEntity = null;

            var mapper = LinqMapper;

            Table<TRepository> table = CurrentContext.GetTable<TRepository>();

            // get the existing repository entity - results in a SQL SELECT statement
            repositoryEntity = table
                .Where(string.Format(CultureInfo.InvariantCulture, "{0} == @0", mapper.QueryParameter), id)
                .SingleOrDefault<TRepository>();

            return repositoryEntity;
        }

        /// <summary>
        /// 
        /// </summary>
        /// <param name="entityObject"></param>
        /// <returns></returns>
        protected virtual TRepository ConvertAndAttach(TEntity entityObject)
        {
            Guard.ArgumentNotNull(entityObject, "entityObject");

            TRepository repositoryEntity = null;

            var mapper = LinqMapper;
            
            Table<TRepository> table = CurrentContext.GetTable<TRepository>();

            // get the existing repository entity - results in a SQL SELECT statement
            repositoryEntity = table
                .Where(string.Format(CultureInfo.InvariantCulture, "{0} == @0", mapper.QueryParameter), entityObject.Id)
                .SingleOrDefault<TRepository>();

            if (repositoryEntity != null)
            {
                mapper.ToRepositoryEntity(entityObject, repositoryEntity);
            }

            return repositoryEntity;
        }

        /// <summary>
        /// 
        /// </summary>
        /// <param name="context"></param>
        /// <param name="expression"></param>
        /// <returns></returns>
        [SuppressMessage("Microsoft.Design", "CA1006:DoNotNestGenericTypesInMemberSignatures",
            Justification = "Uses non-compiled linq expression")]
        protected virtual IQueryable<TEntity> CreateCommonQuery(DataContext context, Expression<Func<TEntity, bool>> expression)
        {
            var result = LinqMapper.AsQueryable(context);

            return expression != null ?
                (result.Where(expression)) :
                (result);
        }

        /// <summary>
        /// 
        /// </summary>
        /// <param name="context"></param>
        protected virtual void ResolveConflicts(DataContext context)
        {
            Guard.ArgumentNotNull(context, "context");

            context.ChangeConflicts.ResolveAll(RefreshMode.KeepChanges);

            context.SubmitChanges(ConflictMode.FailOnFirstConflict);
        }

        private IQueryable<TEntity> ConstructLinqQuery(Expression<Func<TEntity, bool>> expression)
        {
            var query = CreateCommonQuery(CurrentContext, expression);

            if (CacheQuery)
            {
                query = LinqEntityCompiledQuery<TEntity>.PreCompile(CurrentContext, query);
            }

            return query;
        }

        #endregion
    }
}
